package img;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.tika.Tika;
import org.apache.tika.mime.MimeType;
import org.apache.tika.mime.MimeTypes;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

/**
 * 获取文件类型
 * 通过文件后缀来判断文件类型并不安全
 * https://github.com/hiwepy/validation-mimetypes-spring-boot-starter/blob/master/src/main/java/com/github/hiwepy/validation/utils/FiletypeUtils.java
 * java 通过 MagicNumber 魔数 获取文件类型，并校验 https://blog.csdn.net/coquetish_girl/article/details/135148201
 *
 * Java文件类型校验之Apache Tika https://blog.csdn.net/qq_43602576/article/details/128645634
 * Java使用tika工具类检查文件真实类型,防止文件伪造后缀名进行攻击 https://www.jianshu.com/p/2eb5adb0342b
 *
 * 服务器对上传文件一般进行文件类型的严格限制,防止有恶意文件上传至服务器。
 * 一般使用“后缀名”方式去鉴别上传文件类型，但是该种方式有可能被绕过。
 * 恶意攻击者通过将非法文件修改为合法的后缀名方式提交文件至服务器，从而调用恶意脚本。
 *
 * 例如：
 * 通过将.jsp后缀运行文件更改后缀名为.jpg方式伪装为图片提交至服务器，因为服务端对于图片格式文件允许运行，
 * 则导致恶意.jsp源文件得以运行造成影响
 *
 * xlsx,ppt,pptx本质上就是zip，文件开头前缀是一样的，所以需要额外判断
 * 纯js判断文件流格式类型：pdf,doc,docx,xls,xlsx,ppt,pptx一次搞定 https://blog.csdn.net/csdnyiiran/article/details/128735272
 * https://github.com/react-office-viewer/getFileTypeFromArrayBuffer
 * https://www.cnblogs.com/SZPLove/p/15740503.html
 */
public class FiletypeUtils {

    public static void main(String[] args) throws Exception {
        FiletypeUtils filetypeUtils = new FiletypeUtils();
        // filetypeUtils.getMimeType("C:\\Users\\74102\\Pictures\\文件类型\\新建 DOCX 文档 (2) - 副本.zip.png"); // .zip
        // filetypeUtils.getMimeType("C:\\Users\\74102\\Pictures\\文件类型\\188.tar.png"); // .gtar
        // filetypeUtils.getMimeType("C:\\Users\\74102\\Pictures\\文件类型\\手机创建docx222.docx"); // .docx
        // filetypeUtils.getMimeType("C:\\Users\\74102\\Pictures\\文件类型\\敏感词汇总.xlsx"); // .xlsx
        // filetypeUtils.getMimeType("C:\\Users\\74102\\Pictures\\文件类型\\敏感词汇总 - 副本.xlsx.zip"); // .zip xlsx改后缀为zip
        // filetypeUtils.getMimeType("C:\\Users\\74102\\Pictures\\文件类型\\188.tar - 副本.zip.xlsx"); // .xlsx zip改后缀为xlsx
        // filetypeUtils.getMimeType("C:\\Users\\74102\\Pictures\\logo.jpg"); // .jpg

        // getFileType("C:\\Users\\74102\\Pictures\\文件类型\\敏感词汇总 - 副本.xlsx.zip"); // zip 504b0304140006000800
        // getFileType("C:\\Users\\74102\\Pictures\\文件类型\\新建 DOCX 文档 (2) - 副本.zip.png"); // zip 504b0304140000000800
        // getFileType("C:\\Users\\74102\\Pictures\\logo.jpg"); // jpg ffd8ffe000104a464946

        System.out.println(isOffice2007("C:\\Users\\74102\\Pictures\\文件类型\\敏感词汇总 - 副本.xlsx.zip", "xlsx"));
        System.out.println(isOffice2007("C:\\Users\\74102\\Pictures\\文件类型\\手机创建docx222.docx", "docx"));

        System.out.println(hexStringToAscii("77 6F 72 6B 73 68 65 65 74 73 2F".replace(" ", ""))); // 字母大写 worksheets/
        System.out.println(hexStringToAscii("77 6f 72 6b 73 68 65 65 74 73 2f".replace(" ", ""))); // 字母小写 worksheets/

    }

    /**
     * 通过tika工具检测文件实际类型 不太能分清xlsx、docx、zip，因为xlxs、docxde1本质就是zip
     * spring boot + Apache tika 实现文档内容解析 https://blog.csdn.net/qq_43672627/article/details/131553883 跑不起来
     *
     * Java文件类型校验之Apache Tika https://blog.csdn.net/qq_43602576/article/details/128645634
     */
    public static String getMimeType(String filePath) throws Exception {
        // 获取文件
        // File file = ResourceUtils.getFile(filePath);
        File file = new File(filePath);
        if (!file.exists() || !file.isFile()) {
            System.out.println("文件不存在");
            return null;
        }

        // 使用tika提供的外观工具,进行检测
        Tika tika = new Tika();
        // 此处检测文件内容,返回文件MimeType名称
        String detect = tika.detect(file);
        System.out.println("MimeType:" + detect);

        // 获取tika提供的默认参照表
        // 可以进行自定义,参照https://stackoverflow.com/questions/13650372/how-to-determine-appropriate-file-extension-from-mime-type-in-java
        MimeTypes allTypes = MimeTypes.getDefaultMimeTypes();
        // 根据MimeType名称获取MimeType类型
        MimeType mimeType = allTypes.forName(detect);

        // 根据MimeType类型获取对应的后缀名
        String extension = mimeType.getExtension();
        System.out.println(extension);

        return extension;
    }

    public static String getFileType(String filePath) {
        // 获取文件
        File file = new File(filePath);
        if (!file.exists() || !file.isFile()) {
            System.out.println("文件不存在");
            return null;
        }

        // 文件转byte数组
        byte[] bytes = fileToByteArray(file);
        return getFileType(bytes, filePath);
    }

    /**
     * 获取文件类型
     * Java根据文件字节流判断文件类型：https://www.suibibk.com/topic/743151941151883264
     */
    public static String getFileType(byte[] bytes, String filePath) {

        if (bytes == null || bytes.length < 11) {
            return null;
        }

        // 文件后缀（文件类型）
        String fileSuffix = null;
        Map<String, String> fileTypes = getAllFileType();

        // 先找前10-->前5-->前3
        String header = null;
        if (StringUtils.isEmpty(fileSuffix)) {
            header = bytesToHexString(ArrayUtils.subarray(bytes, 0, 10)); // 504b0304140006000800
            System.out.println(filePath + " header：" + header);
            fileSuffix = fileTypes.get(header);
        }

        // 前5
        if (StringUtils.isEmpty(fileSuffix)) {
            Iterator keyList = fileTypes.keySet().iterator();
            // 并不是所有的文件格式前10 byte（jpg）都一致，前五个byte一致即可
            String key, keySearchPrefix = header.substring(0, 5 * 2);
            while (keyList.hasNext()) {
                key = (String) keyList.next();
                if (key.contains(keySearchPrefix)) {
                    fileSuffix = fileTypes.get(key);
                    break;
                }
            }
        }

        // 前3
        if (StringUtils.isEmpty(fileSuffix)) {
            Iterator keyList = fileTypes.keySet().iterator();
            String key, keySearchPrefix = header.substring(0, 3 * 2);
            while (keyList.hasNext()) {
                key = (String) keyList.next();
                if (key.contains(keySearchPrefix)) {
                    fileSuffix = fileTypes.get(key);
                    break;
                }
            }
        }

        System.out.println("文件类型：" + fileSuffix);
        return fileSuffix;
    }

    /**
     * 是否是指定类型的office2007
     * 纯js判断文件流格式类型：pdf,doc,docx,xls,xlsx,ppt,pptx一次搞定 https://blog.csdn.net/csdnyiiran/article/details/128735272
     * 区分offfice2007：xlsx,pptx,docx，3种文件的文件头标识信息是一样的，但文件结尾位置的关键词不一样
     * docx文件：word/
     * xlsx文件：worksheets/
     * pptx文件：ppt/
     */
    private static boolean isOffice2007(String filePath, String officeFileType) {
        // 获取文件
        File file = new File(filePath);
        if (!file.exists() || !file.isFile()) {
            System.out.println("文件不存在");
            return false;
        }

        int loc = file.getPath().lastIndexOf(".");
        // 文件后缀
        String fileExt = file.getPath().substring(loc + 1);

        if (fileExt == null || !Arrays.asList("xlsx", "docx", "pptx").contains(fileExt.toLowerCase())) {
            return false;
        }

        // 先判断大类 文件开头 是不是zip
        // 再判断小类 文件结尾
        /**
         *  区分xlsx,pptx,docx三种格式的buffer码。通过每个文件末尾的关键词检索判断
         *   const format2007Map = {
         *     xlsx: ['77 6F 72 6B 73 68 65 65 74 73 2F'], // 转换成ascii码的含义是 worksheets/ 字母大写
         *     docx: ['77 6F 72 64 2F'], // 转换成ascii码的含义是 word/
         *     pptx: ['70 70 74 2F'] // 转换成ascii码的含义是 ppt/
         *   };
         */
        // 文件转byte数组
        byte[] bytes = fileToByteArray(file);
        String fileType = getFileType(bytes, filePath);

        if (!"zip".equals(fileType)) {
            return false;
        }

        // 先找后500-->后1500-->后3000
        String endFileType = null;
        endFileType = getFileTypeByEnd(filePath, bytes, endFileType, 500);
        endFileType = getFileTypeByEnd(filePath, bytes, endFileType, 1500);
        endFileType = getFileTypeByEnd(filePath, bytes, endFileType, 3000);

        if (StringUtils.isNotBlank(endFileType) && endFileType.equals(officeFileType)) {
            return true;
        }

        return false;
    }

    private static String getFileTypeByEnd(String filePath, byte[] bytes, String endFileType, int size) {
        if (StringUtils.isBlank(endFileType) && bytes.length > size) {
            Map<String, String> format2007Map = getFormat2007Map();
            String end = bytesToHexString(ArrayUtils.subarray(bytes, bytes.length - size, bytes.length));
            System.out.println(filePath + " end：" + end);

            for (Map.Entry<String, String> entry : format2007Map.entrySet()) {
                String key = entry.getKey().replace(" ", "").toLowerCase();
                String value = entry.getValue();

                if (end.indexOf(key) > -1) {
                    endFileType = value;
                    break;
                }
            }
        }
        return endFileType;
    }

    /**
     * 文件转byte数组
     * https://blog.csdn.net/tttzzzqqq2018/article/details/132613467
     */
    private static byte[] fileToByteArray(File file) {
        try (FileInputStream fis = new FileInputStream(file);
             ByteArrayOutputStream bos = new ByteArrayOutputStream()) {

            byte[] buffer = new byte[1024];
            int bytesRead;
            while ((bytesRead = fis.read(buffer)) != -1) {
                bos.write(buffer, 0, bytesRead);
            }
            return bos.toByteArray();

        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 文件转换成二进制流（byte数组），再转化成16进制（MagicNumber(魔数)）
     *
     * @param src
     * @return 16进制 字母都是小写
     */
    private static String bytesToHexString(byte[] src) {
        StringBuilder stringBuilder = new StringBuilder();
        if (src == null || src.length <= 0) {
            return null;
        }
        for (int i = 0; i < src.length; i++) {
            int v = src[i] & 0xFF;
            String hv = Integer.toHexString(v); // toHexString()方法返回的十六进制字符串都是小写字母形式
            if (hv.length() < 2) {
                stringBuilder.append(0); // 不足2位，前面补0
            }
            stringBuilder.append(hv);
        }
        return stringBuilder.toString();
    }

    /**
     * 常用文件格式 全部小写
     */
    public static Map<String, String> getAllFileType() {
        Map<String, String> fileTypeMap = new HashMap<>();
        fileTypeMap.put("504b0304140000000800", "zip"); // 前十位
        fileTypeMap.put("504b030414", "zip"); // 前5位 2个数字表示1个byte位
        fileTypeMap.put("504b03", "zip"); // 前5位 2个数字表示1个byte位

        fileTypeMap.put("ffd8ffe000104a464946", "jpg"); // JPEG (jpg)
        fileTypeMap.put("89504e470d0a1a0a0000", "png"); // PNG (png)
        fileTypeMap.put("47494638396126026f01", "gif"); // GIF (gif)
        fileTypeMap.put("49492a00227105008037", "tif"); // TIFF (tif)
        fileTypeMap.put("424d228c010000000000", "bmp"); // 16色位图(bmp)
        fileTypeMap.put("424d8240090000000000", "bmp"); // 24位位图(bmp)
        fileTypeMap.put("424d8e1b030000000000", "bmp"); // 256色位图(bmp)
        fileTypeMap.put("41433130313500000000", "dwg"); // CAD (dwg)

        fileTypeMap.put("7b5c727466315c616e73", "rtf"); // Rich Text Format (rtf)
        fileTypeMap.put("38425053000100000000", "psd"); // Photoshop (psd)
        fileTypeMap.put("46726f6d3a203d3f6762", "eml"); // Email [Outlook Express 6] (eml)
        fileTypeMap.put("d0cf11e0a1b11ae10000", "doc"); // MS Excel 注意：word、msi 和 excel的文件头一样
        fileTypeMap.put("d0cf11e0a1b11ae10000", "vsd"); // Visio 绘图
        fileTypeMap.put("5374616E64617264204A", "mdb"); // MS Access (mdb)
        fileTypeMap.put("252150532D41646F6265", "ps");
        fileTypeMap.put("255044462d312e350d0a", "pdf"); // Adobe Acrobat (pdf)
        fileTypeMap.put("2e524d46000000120001", "rmvb"); // rmvb/rm相同
        fileTypeMap.put("464c5601050000000900", "flv"); // flv与f4v相同
        fileTypeMap.put("00000020667479706d70", "mp4");
        fileTypeMap.put("49443303000000002176", "mp3");
        fileTypeMap.put("000001ba210001000180", "mpg"); //
        fileTypeMap.put("3026b2758e66cf11a6d9", "wmv"); // wmv与asf相同
        fileTypeMap.put("52494646e27807005741", "wav"); // Wave (wav)
        fileTypeMap.put("52494646d07d60074156", "avi");
        fileTypeMap.put("4d546864000000060001", "mid"); // MIDI (mid)

        fileTypeMap.put("526172211a0700cf9073", "rar");
        fileTypeMap.put("235468697320636f6e66", "ini");
        fileTypeMap.put("4d616e69666573742d56", "mf");// MF文件
        fileTypeMap.put("3c3f786d6c2076657273", "xml");// xml文件
        fileTypeMap.put("1f8b0800000000000000", "gz");// gz文件
        fileTypeMap.put("d0cf11e0a1b11ae10000", "wps");// WPS文字wps、表格et、演示dps都是一样的

        fileTypeMap.put("6D6F6F76", "mov"); // Quicktime (mov)
        fileTypeMap.put("FF575043", "wpd"); // WordPerfect (wpd)
        fileTypeMap.put("CFAD12FEC5FD746F", "dbx"); // Outlook Express (dbx)
        fileTypeMap.put("2142444E", "pst"); // Outlook (pst)
        fileTypeMap.put("AC9EBD8F", "qdf"); // Quicken (qdf)
        fileTypeMap.put("E3828596", "pwl"); // Windows Password (pwl)
        fileTypeMap.put("2E7261FD", "ram"); // Real Audio (ram)
        fileTypeMap.put("null", null); // null

        return fileTypeMap;
    }

    /**
     * office2007文件 结尾关键词
     */
    public static Map<String, String> getFormat2007Map() {
        Map<String, String> fileTypeMap = new HashMap<>();
        fileTypeMap.put("77 6F 72 6B 73 68 65 65 74 73 2F", "xlsx"); // 转换成ascii码的含义是 worksheets/ 字母大写
        fileTypeMap.put("77 6F 72 64 2F", "docx"); // 转换成ascii码的含义是 word/
        fileTypeMap.put("70 70 74 2F", "pptx"); // 转换成ascii码的含义是 ppt/

        return fileTypeMap;
    }

    /**
     * 16进制转换为ASCII码
     *
     * @param hexString 字母大小写无关，结果一致
     * @return
     */
    private static String hexStringToAscii(String hexString) {
        byte[] bytes = new byte[hexString.length() / 2];
        for (int i = 0; i < bytes.length; i++) {
            bytes[i] = (byte) Integer.parseInt(hexString.substring(2 * i, 2 * i + 2), 16);
        }
        return new String(bytes);
    }

}